import streamlit as st
import requests
from bs4 import BeautifulSoup
import time
import csv
import random
from urllib.parse import urlparse
from io import StringIO

# 初始化 session state
if 'scraping' not in st.session_state:
    st.session_state.scraping = False
if 'reviews' not in st.session_state:
    st.session_state.reviews = []
if 'logs' not in st.session_state:
    st.session_state.logs = []

# 页面布局
st.set_page_config(
    page_title="豆瓣影评采集器",
    page_icon="🎬",
    layout="wide"
)
st.title("豆瓣影评爬虫 🎥")
st.markdown("---")

# 侧边栏参数配置
with st.sidebar:
    st.header("参数设置 ⚙️")

    # 电影ID输入
    movie_input = st.text_input(
        "电影ID/URL",
        help="请输入豆瓣电影ID或完整URL（例如：34780991 或 https://movie.douban.com/subject/34780991/）",
        value="34780991"
    )


    # 解析电影ID
    def parse_movie_id(input_str):
        if input_str.isdigit():
            return input_str
        try:
            path = urlparse(input_str).path
            return path.split('/')[2]
        except:
            return input_str


    movie_id = parse_movie_id(movie_input)

    # 排序方式
    sort_method = st.selectbox(
        "排序方式",
        options=["hotest", "newest"],
        format_func=lambda x: "最热" if x == "hotest" else "最新"
    )

    # 基础配置
    st.subheader("基础配置")
    max_pages = st.number_input(
        "最大爬取页数",
        min_value=1,
        max_value=50,
        value=5,
        help="每页包含20条评论"
    )
    user_agent = st.text_input(
        "User-Agent",
        value="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
    )

    # 高级配置
    with st.expander("高级配置 🔧", expanded=False):
        st.subheader("身份认证")
        cookie_input = st.text_area(
            "请求Cookie",
            help="格式：name1=value1; name2=value2",
            placeholder="dbcl2=\"123456789\"; bid=abc123...",
            height=100
        )

        st.subheader("网络代理")
        col1, col2 = st.columns(2)
        with col1:
            proxy_http = st.text_input(
                "HTTP代理",
                help="格式：http://ip:port",
                placeholder="http://127.0.0.1:8080"
            )
        with col2:
            proxy_https = st.text_input(
                "HTTPS代理",
                help="格式：https://ip:port",
                placeholder="https://127.0.0.1:8080"
            )

        st.subheader("请求控制")
        delay_range = st.slider(
            "请求间隔（秒）",
            min_value=1.0,
            max_value=10.0,
            value=(2.0, 5.0),
            help="随机延迟区间"
        )

    # 输出字段
    st.subheader("输出设置")
    selected_fields = st.multiselect(
        "输出字段",
        options=['标题', '作者', '评分', '发布时间', '内容'],
        default=['标题', '作者', '评分', '发布时间', '内容']
    )

# 控制按钮
col1, col2 = st.columns(2)
with col1:
    start_btn = st.button("🚀 开始爬取", disabled=st.session_state.scraping)
with col2:
    stop_btn = st.button("⏹️ 停止", disabled=not st.session_state.scraping)

# 日志显示区域
log_container = st.empty()
progress_bar = st.progress(0)


# 爬虫逻辑
def scraper_generator():
    base_url = f"https://movie.douban.com/subject/{movie_id}/reviews"
    headers = {
        'User-Agent': user_agent,
        'Referer': 'https://www.douban.com/'
    }

    # Cookie处理
    cookies = {}
    if cookie_input:
        try:
            cookies = {
                item.split('=')[0].strip(): '='.join(item.split('=')[1:]).strip()
                for item in cookie_input.split(';')
            }
        except Exception as e:
            st.session_state.logs.append(f"⚠️ Cookie解析失败: {str(e)}")

    # 代理处理
    proxies = {}
    if proxy_http:
        proxies['http'] = proxy_http
    if proxy_https:
        proxies['https'] = proxy_https

    current_start = 0
    for page in range(max_pages):
        if st.session_state.get('stop_flag'):
            st.session_state.logs.append(f"⏹️ 用户主动停止爬取")
            break

        try:
            params = {
                "sort": sort_method,
                "start": current_start
            }

            # 发送请求
            response = requests.get(
                base_url,
                params=params,
                headers=headers,
                cookies=cookies,
                proxies=proxies if proxies else None,
                timeout=15,
                verify=False
            )
            response.raise_for_status()

            # 反爬检测
            if "检测到有异常请求" in response.text:
                raise Exception("触发反爬机制")

            # 解析页面
            soup = BeautifulSoup(response.text, 'html.parser')
            items = soup.find_all('div', class_='review-item')

            if not items:
                st.session_state.logs.append(f"⏭️ 第 {page + 1} 页没有数据")
                break

            # 解析逻辑
            for item in items:
                # 标题解析
                title_tag = item.find('h2').find('a')
                title = title_tag.text.strip() if title_tag else "无标题"

                # 作者信息
                author_tag = item.find('a', class_='name')
                author = author_tag.text.strip() if author_tag else "匿名用户"

                # 评分处理
                rating_tag = item.find('span', class_='main-title-rating')
                rating = rating_tag['title'] if rating_tag else "未评分"

                # 发布时间
                time_tag = item.find('span', class_='main-meta')
                time_published = time_tag.text.strip() if time_tag else "时间未知"

                # 内容提取
                content_div = item.find('div', class_='short-content')
                content = ""
                if content_div:
                    content_parts = []
                    for child in content_div.children:
                        if child.name == 'p' and 'spoiler-tip' in child.get('class', []):
                            continue
                        if isinstance(child, str):
                            content_parts.append(child.strip())
                        elif child.name == 'a' and 'unfold' in child.get('class', []):
                            continue
                        else:
                            content_parts.append(child.text.strip())
                    content = ' '.join(content_parts)[:500]

                st.session_state.reviews.append({
                    '标题': title,
                    '作者': author,
                    '评分': rating,
                    '发布时间': time_published,
                    '内容': content
                })

            st.session_state.logs.append(f"✅ 成功抓取第 {page + 1} 页，累计 {len(st.session_state.reviews)} 条")
            current_start += 20
            yield page + 1

            # 随机延迟
            time.sleep(random.uniform(delay_range[0], delay_range[1]))

        except Exception as e:
            st.session_state.logs.append(f"❌ 第 {page + 1} 页失败: {str(e)[:50]}")
            break


# 开始爬取逻辑
if start_btn and not st.session_state.scraping:
    st.session_state.scraping = True
    st.session_state.stop_flag = False
    st.session_state.reviews = []
    st.session_state.logs = []

    # 启动爬虫
    scraper = scraper_generator()

    try:
        for current_page in scraper:
            # 更新进度
            progress = current_page / max_pages
            progress_bar.progress(progress)

            # 显示实时日志
            log_container.write("\n".join(st.session_state.logs[-5:]))

            # 模拟实时更新
            time.sleep(0.1)

    except StopIteration:
        pass

    st.session_state.scraping = False
    st.rerun()

# 停止逻辑
if stop_btn and st.session_state.scraping:
    st.session_state.stop_flag = True
    st.session_state.scraping = False
    st.rerun()

# 结果显示
if st.session_state.reviews:
    st.markdown("---")
    st.subheader("抓取结果 📊")

    # 显示统计数据
    col1, col2, col3 = st.columns(3)
    col1.metric("总评论数", len(st.session_state.reviews))
    col2.metric("有评分评论",
                len([r for r in st.session_state.reviews if r['评分'] != "未评分"]))
    col3.metric("平均内容长度",
                f"{sum(len(r['内容']) for r in st.session_state.reviews) // len(st.session_state.reviews)} 字符")

    # 数据预览
    with st.expander("数据预览 👀"):
        st.dataframe(
            st.session_state.reviews[:10],
            column_config={
                "内容": st.column_config.TextColumn(width="large")
            },
            height=300
        )

    # 数据保存板块
    with st.expander("数据保存设置 💾", expanded=True):
        # 自动保存路径设置
        import os

        default_path = os.path.join(
            os.path.expanduser('~'),
            'Documents',
            f'douban_{movie_id}_reviews.csv'
        )
        save_path = st.text_input(
            "本地保存路径",
            value=default_path,
            help="请输入完整文件路径"
        )


        # CSV生成函数（移到按钮外部）
        def generate_csv(fields, data, path=None):
            """生成CSV内容"""
            csv_buffer = StringIO()
            csv_buffer.write('\ufeff')  # BOM头
            writer = csv.writer(csv_buffer,
                                delimiter=',',
                                quotechar='"',
                                quoting=csv.QUOTE_MINIMAL)

            # 写入表头
            writer.writerow(fields)

            # 写入数据
            for row in data:
                cleaned_row = [
                    str(row[field])
                    .replace('\n', ' ')
                    .replace('\r', '')
                    .replace('"', '""')
                    for field in fields
                ]
                writer.writerow(cleaned_row)

            return csv_buffer.getvalue()


        # 立即保存按钮
        col1, col2 = st.columns(2)
        with col1:
            if st.button("💾 立即保存数据到本地"):
                if not selected_fields:
                    st.error("请至少选择一个输出字段")
                    st.session_state.logs.append("❌ 保存失败：未选择输出字段")
                else:
                    try:
                        # 生成CSV内容
                        csv_content = generate_csv(
                            selected_fields,
                            st.session_state.reviews
                        )

                        # 创建目录（如果不存在）
                        os.makedirs(os.path.dirname(save_path), exist_ok=True)

                        # 保存文件
                        with open(save_path, 'w', encoding='utf-8-sig') as f:
                            f.write(csv_content)

                        st.success(f"数据成功保存至：{save_path}")
                        st.session_state.logs.append(f"💾 数据已保存至：{save_path}")
                    except PermissionError as e:
                        st.error(f"权限不足：{str(e)}")
                        st.session_state.logs.append(f"❌ 保存失败：{str(e)}")
                        st.info("建议：1. 更换保存路径 2. 以管理员身份运行 3. 关闭正在使用的文件")
                    except Exception as e:
                        st.error(f"保存失败：{str(e)}")
                        st.session_state.logs.append(f"❌ 保存失败：{str(e)}")

        # 下载按钮
        with col2:
            if st.button("📥 生成下载文件"):
                if not selected_fields:
                    st.error("请至少选择一个输出字段")
                else:
                    try:
                        csv_content = generate_csv(
                            selected_fields,
                            st.session_state.reviews
                        )
                        st.download_button(
                            label="下载CSV文件",
                            data=csv_content.encode('utf-8-sig'),
                            file_name=f'douban_{movie_id}_reviews.csv',
                            mime='text/csv'
                        )
                    except Exception as e:
                        st.error(f"生成下载文件失败：{str(e)}")

# 显示完整日志
if st.session_state.logs:
    with st.expander("完整日志 📜"):
        st.code("\n".join(st.session_state.logs))

# 使用说明
with st.expander("使用说明 📖"):
    st.markdown("""
    ### 基础使用
    1. ​**输入电影标识**​  
       支持直接输入豆瓣电影ID或完整URL  
       ✅ 正确示例：`34780991` 或 `https://movie.douban.com/subject/34780991/`

    2. ​**配置爬取参数**​  
       - 排序方式：最热评论（默认）或最新评论
       - 最大页数：每页包含20条评论
       - User-Agent：保持默认即可，遇到反爬时可修改

    3. ​**选择输出字段**​  
       勾选需要导出的数据列，默认全选

    ### 高级配置
    ​**身份认证**​  
    - 获取Cookie：  
      1. 浏览器登录豆瓣账号
      2. 打开开发者工具（F12）
      3. 访问目标电影页面
      4. 复制请求头中的Cookie值
    - 格式要求：`name1=value1; name2=value2`

    ​**网络代理**​  
    - HTTP/HTTPS代理需要分别配置
    - 支持格式：`协议://IP地址:端口`
    - 推荐代理服务：Luminati、SmartProxy

    ​**请求控制**​  
    - 随机延迟：建议设置在2-5秒之间
    - 超时时间：默认15秒，根据网络情况调整

    ### 注意事项
    - 请合理控制请求频率，遵守豆瓣 robots.txt
    - 高级配置信息仅保存在当前会话中
    - 遇到验证码时可尝试更换代理IP
    - 本工具仅限学习研究使用，禁止商业用途
    """)

# 页脚
st.markdown("---")
st.caption("🛠️ 由 Streamlit 驱动 | 📅 数据实时采集 | 🔒 隐私安全保证")